iOS 编码规范整理
本文参考自
《苹果Cocoa编码规范》
《objective-c-style-guide》
,希望能够启动抛砖引玉的功能。而且会持续更新,逐步细化。
命名规则
一般原则
清晰
命名最好清晰又简洁,但是不能过于简洁而失去了清晰。
1
2insertObject:atIndex: //这是个好例子
insert:at: //不够清晰,插入的是什么?at又指的是什么?名称通常不缩写,即使名称很长,也要拼写完全
代码 | 点评 |
---|---|
destinationSelection | Good. |
destSel | Not clear. |
setBackgroundColor: | Good. |
setBkgdColor: | Not clear. |
关于缩写,你可能会认为某个缩写广为人知,但有可能并非如此,尤其是当你的代码被来自不同文化和语言背景的开发人员所使用时。当然,你可以使用少数非常常见,历史悠久的缩写。所有可以使用的缩写见这个列表
建议:类、变量和函数命名参考苹果的头文件及优秀的开源项目。
一致性
- 尽可能使用与 Cocoa 编程接口命名保持一致的名称。
- 在使用多态方法的类中,命名的一致性非常重要。在不同类中实现相同功能的方法应该具有相同的名称
前缀
由于苹果没有命名空间,因此使用前缀可以防止和苹果以及其他第三方库的命名冲突。
- 不要使用下划线或子前缀
- 对class、protocol、structure、公开方法、常量命名时使用前缀,成员方法和结构体字段时不使用前缀。
- Category 方法统一使用xy_methodName 形式进行扩展
书写约定
- 命名时单词之间不要使用下划线、破折号等标点符号分隔,请使用驼峰命名法对方法、变量进行命名
如果方法名使用一个广为人知的大写首字母缩略词开头,则首字母可以大些。如NSImage
中的TIFFRepresentation如果方法名或者常量名使用了前缀,则前缀之后所有单词的首字母都要大写。如NSRunAlertPanel
避免使用下划线来表示名称的私有属性。苹果公司保留该方式的使用。如果第三方这样使用可能会导致命名冲突,他们可能会在无意中用自己的方法覆盖掉已有的私有方法,这会导致严重的后果
类和协议的命名
类名应该包含明确描述该类/对象是什么或者做什么的名词,类名要有合适的前缀。
协议应根据它包含的方法的作用来命名。
- 大多数协议仅组合一组相关的方法,而不关联任何类,这种协议的命名应该使用动名词(ing),以不与类名混淆 比如NSLocking
- 有些协议组合一些彼此无关的方法(这样做是避免创建多个独立的小协议)。这样的协议倾向于与某个类关联在一起,该类是协议的主要体现者。在这种情形,我们约定协议的名称与该类同名。NSObject 协议就是这样一个例子。这个协议组合一组彼此无关的方法,有用于查询对象在其类层次中位置的方法,有使之能调用特殊方法的方法以及用于增减引用计数的方法。由于 NSObject 是这些方法的主要体现者,所以我们用类的名称命名这个协议。
头文件
- 声明孤立的类或协议:将孤立的类或协议声明放置在单独的头文件中,该头文件名称与类或协议同名
- 声明相关联的类或协议:将相关联的声明(类,类别及协议) 放置在一个头文件中,该头文件名称与主要的类/类别/协议的名字相同
1
2
3
4
5
6
7
8
9
10
11@protocol SDWebImageManagerDelegate <NSObject>
@optional
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nullable NSURL *)imageURL;
@end
@interface SDWebImageManager : NSObject
@property (weak, nonatomic, nullable) id <SDWebImageManagerDelegate> delegate;
// ...
@end
方法命名规范
一般规则
小写第一个单词的首字符,大写随后单词的首字符,不使用前缀。有两种例外情况:
1,方法名以广为人知的大写字母缩略词(如TIFF or PDF)开头;
2,私有方法可以使用统一的前缀来分组和辨识表示对象行为的方法,名称以动词开头
1 | - (void)invokeWithTarget:(id)target: |
名称中不要出现 do 或 does,因为这些助动词没什么实际意义。也不要在动词前使用副词或形容词修饰
- 方法返回接收者的某个属性,直接用属性名称命名。
1 | - (NSSize)cellSize; //优 |
只有在方法需要间接返回多个值的情况下,才使用 get
1
2
3 >//NSBezierPath
>- (void) getLineDash:(float *)pattern count:(int *)count phase:(float *)phase;
>
像上面这样的方法,在其实现里应允许接受 NULL 作为其 in/out 参数,以表示调用者对一个或多个返回值不感兴趣。
- 参数前面的单词要能描述该参数
1 | - (void)sendAction:(SEL)aSelector to:(id)anObject forAllCells:(BOOL)flag; //优 |
- 细化基类中的已有方法:创建一个新方法,其名称是在被细化方法名称后面追加参数关键词
1 | //NSView |
- 不要使用 and 来连接用属性作参数关键字
1 | - (int) runModalForDirectory:(NSString *)path file:(NSString *)name types:(NSArray *)fileTypes; //优 |
###访问方法
- 如果属性是用名词描述的,则命名格式为:
1 | - (type)noun; |
如:
1 | - (NSColor *)color; |
- 如果属性是用形容词描述的,则命名格式为:
1 | - (BOOL)isAdjective; |
如:1
2- (BOOL)isEditable;
- (void)setEditable:(BOOL)flag;
- 如果属性是用动词描述的,则命名格式为:(动词要用现在时时态)
1 | - (BOOL)verbObject; |
例如:1
2- (BOOL)showsAlpha;
- (void)setShowAlpha:(BOOL)flag;
- 不要使用动词的过去分词形式作形容词使用
1 | - (BOOL)acceptsGlyphInfo; //优 |
- 可以使用情态动词(can, should, will 等)来提高清晰性,但不要使用 do 或 does
1 | - (BOOL)canHide; //优 |
Delegate Methods 委托方法
委托方法是那些在特定事件发生时可被对象调用,并声明在对象的委托类中的方法。它们有独特的命名约定,这些命名约定同样也适用于对象的数据源方法。
- 名称以标示发送消息的对象的类名开头,省略类名的前缀并小写类第一个字符
1 | - (BOOL)tableView:(UITableView *)tableView shouldSelectRow:(int)row; |
- 冒号紧跟在类名之后(随后的那个参数表示委派的对象)。该规则不适用于只有一个 sender 参数的方法
1 | - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender; |
- 上面的那条规则也不适用于响应通知的方法。在这种情况下,方法的唯一参数表示通知对象
1 | - (void)windowDidChangeScreen:(NSNotification *)notification; |
- 用于通知委托对象操作即将发生或已经发生的方法名中要使用 did 或 will
1 | - (void)browserDidScroll:(NSBrowser *)sender; |
- 用于询问委托对象可否执行某操作的方法名中可使用 did 或 will,但最好使用 should
1 | - (BOOL)windowShouldClose:(id)sender; |
####Collection Methods 集合方法
- 管理对象(集合中的对象被称之为元素)的集合类,约定要具备如下形式的方法:
1 | - (void)addElement:(elementType)adObj; |
例如:1
2
3- (void)addLayoutManager:(NSLayoutManager *)adObj;
- (void)removeLayoutManager:(NSLayoutManager *)anObj;
- (NSArray *)layoutManagers;
集合方法命名有如下一些限制和约定:
- 集合中的元素无序,返回 NSSet,而不是 NSArray
- 将元素插入指定位置的功能很重要,则需具备如下方法:
1 | - (void)insertElement:(elementType)anObj atIndex:(int)index; |
集合方法的实现要考虑如下细节:
- 以上集合类方法通常负责管理元素的所有者关系,在 add 或 insert 的实现代码里会 retain 元素,在 remove 的实现代码中会 release 元素
- 当被插入的对象需要持有指向集合对象的指针时,通常使用 set… 来命名其设置该指针的方法,且不要 retain 集合对象。比如上面的 insertLayerManager:atIndex: 这种情形,NSLayoutManager 类使用如下方法:
1 | - (NSTextStorage *)textStorage; |
通常你不会直接调用 setTextStorage:,而是覆写它。另一个关于集合约定的例子来自 NSWindow 类:1
2
3
4
5- (void)addChildWindow:(NSWindow *)childWin ordered:(NSWindowOrderingMode)place;
- (void)removeChildWindow:(NSWindow *)childWin;
- (NSArray *)childWindows;
- (NSWindow *)parentWindow;
- (void)setParentWindow:(NSWindow *)window;
Method Arguments 方法参数
命名方法参数时要考虑如下规则:
- 如同方法名,参数名小写第一个单词的首字符,大写后继单词的首字符。如:removeObject:(id)anObject
- 不要在参数名中使用 pointer 或 ptr,让参数的类型来说明它是指针
- 避免使用 one, two,…,作为参数名
- 避免为节省几个字符而缩写
按照 Cocoa 惯例,以下关键字与参数联合使用:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16...action:(SEL)aSelector
...alignment:(int)mode
...atIndex:(int)index
...content:(NSRect)aRect
...doubleValue:(double)aDouble
...floatValue:(float)aFloat
...font:(NSFont *)fontObj
...frame:(NSRect)frameRect
...intValue:(int)anInt
...keyEquivalent:(NSString *)charCode
...length:(int)numBytes
...point:(NSPoint)aPoint
...stringValue:(NSString *)aString
...tag:(int)anInt
...target:(id)anObject
...title:(NSString *)aString
rivate Methods 私有方法
大多数情况下,私有方法命名相同与公共方法命名约定相同,但通常我们约定给私有方法添加前缀,以便与公共方法区分开来。即使这样,私有方法的名称很容易导致特别的问题。当你设计一个继承自 Cocoa framework 某个类的子类时,你无法知道你的私有方法是否不小心覆盖了框架中基类的同名方法。
Cocoa framework 的私有方法名称通常以下划线作为前缀(如:_fooData),以标示其私有属性。基于这样的事实,遵循以下两条建议:
- 不要使用下划线作为你自己的私有方法名称的前缀,Apple 保留这种用法。
- 若要继承 Cocoa framework 中一个超大的类(如:NSView),并且想要使你的私有方法名称与基类中的区别开来,你可以为你的私有方法名称添加你自己的前缀。这个前缀应该具有唯一性,如基于你公司的名称,或工程的名称,并以“XX_”形式给出。比如你的工程名为”Byte Flogger”,那么就可以是“BF_addObject:” 尽管为私有方法名称添加前缀的建议与前面类中方法命名的约定冲突,这里的意图有所不同:为了防止不小心地覆盖基类中的私有方法。
变量
一般变量
变量使用驼峰命名法。命名时应使用完整的能表示变量作用的词语,禁止使用缩略词或者意义表示不明的词语。错误例子
1 | int w; |
正确例子1
2
3
4
5int numErrors;
int numCompletedConnections;
tickets = [[NSMutableArray alloc] init];
userInfo = [someObject object];
port = [network port];
常量
常量名称前统一加字母“k”,也可以是你认为可以表示为常量的字母,比如“c”1
2
3
4
5
6const int kNumberOfFiles = 12;
NSString *const kUserKey = @"kUserKey";
enum DisplayTinge {
kDisplayTingeGreen = 1,
kDisplayTingeBlue = 2
};
空格与格式
- 方法的声明和定义在-号或者+号与返回值之间应留一个空格。而返回值与方法名以及方法名和参数列表之间都不应该有空格例如
1 | - (void)doSomethingWithString:(NSString *)theString { ...} |
推荐使用容器符号([]以及{})来表示数组和字典。容器符号与内容之间应使用空格分开
1
2 NSArray* array = @[ [foo description], @"Another String", [bar description] ];
NSDictionary* dict = @{ NSForegroundColorAttributeName : [NSColor redColor] };
注释
声明注释
每个类、category和protocol都需在注释中说明它的用途以及在整个框架中的作用变量注释
尽可能详细地对关键变量进行注释,说明其作用- 语句注释
在核心代码或者关键逻辑处必须进行注释,注释时不光要说明这段代码的作用,更要描述清楚为什么要这么做
排版
关键词和操作符之间加适当的空格。 [必须]
正确的示范:1
2
3int count = 5; //注意 = 之间的空格
错误的示范:
int count=5;相对独立的程序块与块之间加空行。 [建议]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17正确的示范:
if(retryCount > 3) {
return;
}
//注意这里空一行
for(int i = 0; i < 100; i++) {
//…do something
}
//注意这里空一行
错误的示范:
int nCount=5;
if(nRetryCount > 3) {
return;
}
for(int i= 0; i < 100; i++) {
//…do something
}方法之间需要空一行。 [必须]
单行代码较长时、表达式等要分成多行书写,划分出的新行要进行适应的缩进,使排版整齐,语句可读。
1
2
3
4
5
6正确的示范:
[_xyCameraMgr changePIPSourceRegion:willMoveSourceIndex
transX:transX/lastPIPZoomScale
transY:transY/lastPIPZoomScale];
错误的示范:
[_xyCameraMgr changePIPSourceRegion:willMoveSourceIndex transX:transX/lastPIPZoomScale transY:transY/lastPIPZoomScale];长表达式要在低优先级操作符处划分新行,操作符放在新行之尾。 [建议]
【注:对于放在新行之首,还是之尾,有一个习惯的问题。每个团队需统一口径。】1
2
3
4
5
6正确的示范:
( ((unsigned int)(x)) & 0xff000000 ) | \
(( ((unsigned int)(x)) & 0x00ff0000) >> 16) | \
(((unsigned int)(x)) & 0x0000ff00 ) | \
(( ((unsigned int)(x)) & 0x000000ff) << 16) \ )循环、判断等语句中若有较长的表达式或语句,则要进行适应的划分。
- 若函数或过程中的参数较长,则要进行适当的划分。
不允许把多个短语句写在一行中,即一行只写一条语句。 [必须]
1
2
3
4
5正确的示范:
int i = 0;
int j = 0;
错误的示范:
int i=0;int j=0;函数或过程的开始、结构的定义及循环、判断等语句中的代码都要采用缩进风格。 [建议]
1
2
3
4
5
6
7
8
9正确的示范:
if(xxx){
//do something
}
错误的示范:
if(xxx)
{
//do something
}
编程实践
UIKit 类实例变量命名规则
UIXXX(标签名) *xx(自定义)+XXX(标签名)
例子:1
2
3
4
5
6
7
UILabel *xxLabel;
UIButton *xxButton;
UIImageView *xxImageView;
UIView *xxView;
UIViewController *xxViewController;
...
Foundation 类实例变量
1 |
|
除了NSString,NSArray,NSDictionary 和它们的Mutable类之外都按照下面规则
NSXXX(标签名) *xx(自定义)+XXX(标签名)
例子:1
2
3
4
5
6
7
8
9
10NSData *xxData;
NSBundle *xxBundle;
NSTimer *xxTimer;
NSURL *xxURL;
NSIndexSet *xxIndexSet;
\\ 容器
NSSet, NSMutableSet
NSCache
NSNumber
...
语法糖
代码中使用语法糖,提高写代码效率
NSArray
1 | //NSArray的定义 |
NSDictionary
1 | //NSDictionary的定义简化 |
NSNumber
1 | NSNumber *a = @123; |
枚举使用
当使用enum时,推荐使用新的固定基本类型规格,因为它有更强的类型检查和代码补全。现在SDK有一个宏NS_ENUM()来帮助和鼓励你使用固定的基本类型。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25例如:
typedef NS_ENUM(NSInteger, RWTLeftMenuTopItemType) {
RWTLeftMenuTopItemMain,
RWTLeftMenuTopItemShows,
RWTLeftMenuTopItemSchedule
};
你也可以显式地赋值(展示旧的k-style常量定义):
typedef NS_ENUM(NSInteger, RWTGlobalConstants) {
RWTPinSizeMin = 1,
RWTPinSizeMax = 5,
RWTPinCountMin = 100,
RWTPinCountMax = 500,
};
旧的k-style常量定义应该避免除非编写Core Foundation C的代码。
不应该:
enum GlobalConstants {
kMaxPinSize = 5,
kMaxPinCount = 500,
};
属性
1 | @property (nonatomic, readwrite, copy) NSString *name; |
建议定义属性的时候把所有的属性特质写全,尤其是如果想定义成只读的(防止外面修改)那一定要加上readonly, 这也是代码安全性的一个习惯.
私有属性
私有属性应该在类的实现文件中的类扩展(匿名分类)中声明.1
2
3
4
5
6
7@interface RWTDetailViewController ()
@property (strong, nonatomic) GADBannerView *googleAdView;
@property (strong, nonatomic) ADBannerView *iAdView;
@property (strong, nonatomic) UIWebView *adXWebView;
@end
Block
Block使用时一定要注意循环引用的问题。1
2
3
4
5
6__weak typeof(self) weakSelf = self;
dispatch_block_t block = ^{
[weakSelf doSomething]; // weakSelf != nil
// preemption, weakSelf turned nil
[weakSelf doSomethingElse]; // weakSelf == nil
};
如此在上面定义一个weakSelf,然后在block体里面使用该weakSelf就可以避免循环引用的问题.很不幸,还是有问题。问题是block体里面的self是weak的,所以就有可能在某一个时段self已经被释放了, 这时block体里面再使用self那就是nil, 不难想象崩溃就在眼前。解决方法很简单, 就是在block体内define一个strong的self, 然后执行的时候判断下self是否还在, 如果在就继续执行下面的操作, 否则return或抛出异常.
1 | __weak typeof(self) weakSelf = self; |
代码组织
在函数分组和protocol/delegate实现中使用#pragma mark -来分类方法,要遵循以下一般结构:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- (instancetype)init {}
- (void)dealloc {}
- (void)viewDidLoad {}
- (void)viewWillAppear:(BOOL)animated {}
- (void)didReceiveMemoryWarning {}
- (IBAction)submitData:(id)sender {}
- (void)publicMethod {}
- (void)privateMethod {}
- (void)setCustomProperty:(id)value {}
- (id)customProperty {}
- (id)copyWithZone:(NSZone *)zone {}
- (NSString *)description {}
常量使用
常量是容易重复被使用和无需通过查找和代替就能快速修改值。常量应该使用static来声明而不是使用#define,除非显式地使用宏。
应该:1
2static NSString * const RWTAboutViewControllerCompanyName = @"RayWenderlich.com";
static CGFloat const RWTImageThumbnailHeight = 50.0;
不应该:1
2
布尔值
Objective-C使用YES和NO。因为true和false应该只在CoreFoundation,C或C++代码使用。既然nil解析成NO,所以没有必要在条件语句比较。不要拿某样东西直接与YES比较,因为YES被定义为1而一个BOOL能被设置为8位。
这是为了在不同文件保持一致性和在视觉上更加简洁而考虑。
应该:1
2if (someObject) {}
if (![anotherObject boolValue]) {}
不应该:1
2
3
4if (someObject == nil) {}
if ([anotherObject boolValue] == NO) {}
if (isAwesome == YES) {} // Never do this.
if (isAwesome == true) {} // Never do this.
如果BOOL属性的名字是一个形容词,属性就能忽略”is”前缀,但要指定get访问器的惯用名称。例如:
1 | @property (assign, getter=isEditable) BOOL editable; |
在类的头文件中尽量少引入其他头文件
通过向前引用 @class className; 解决编译问题。
在xxx.m 文件引人className 文件。这样可以加快编译速度。
单例使用范例
1 | @implementation XY__Name__ |
编码建议
- 类变量若无特别需要尽量声明为@private
- 建议将所有NSObject的重载方法写在@implementation的顶端。例如init
、copyWithZone:以及dealloc - 不需要把变量在init方法里初始化为0或者nil,这是多此一举
- 不要调用NSObject的new方法,也不要在子类中重载它。可以使用alloc和init方法来实例化对象
- 使用#import来引用OC头文件,使用#include来引用C/C++头文件
- 在init或者dealloc方法里不要使用.操作符来访问成员变量,因为在这些方法执行的过程中成员变量将处在不确定的状态
1 | - (instancetype)init { |
- 在dealloc中按照声明顺序来析构变量
- 所有的NSString变量都要使用copy关键字
- 判断BOOL值的时候不要直接把BOOL变量和YES进行比较。BOOL是由char封装而成,它可以表示的内容远远多于YES/NO或者1/0
1 | BOOL isGood; |
- 建议使用@property来声明变量。另外在ARC里不需要再写@ synthesize
了。也不需要再写一个跟property一样的带下划线的私有变量,ARC会自动生成一个下划线的变量的 使用@符号来定义一个NSNumber,这样更简单
1
2NSNumber *number1 = @1; //建议方式
NSNumber *number2 = [NSNumber numberWithInteger:2]; //过时的方式添加新文件时,注意放置的目录,不要把文件放到根目录下面。之前项目文件都在一个目录下,新的独立功能鼓励先建一个新的文件夹,再拖入Xcode中,所有相关的代码文件都放在该文件夹中。
不能引入warning。建议项目中开启Treat Warnings as Errors设置。
但是目前该选项对Swift不起作用,未来苹果完善Swift之后,出现warning会导致编译通不过。
开发过程中注意Xcode控制台输出的警告信息,消除这些警告。目前来看AutoLayout相关的警告会比较多。基于UITableView写页面,基于BaseUITableViewController和RefreshTableViewController作为VC的基类,保证交互的一致性。UITableview构建UI好处如下:
- 1.UITableview是数据驱动的,根据内置的机制做展现。修改dataSource就可以控制界面的展现。刷新界面时只需要使用reloadData和reloadRowsAtIndexPaths就能完成。
- 2.页面扩展能力佳。未来业务发展,需要在页面最上面最下面增加内容,对之前代码做少量修改就能做到。
- 3.大量实现下拉刷新功能的开源项目,如果页面需要刷新功能,轻松做到。
- 4.UITableview具有强大的表现能力,再复杂的页面都可以完成。
- 5.UITableview使得交互更加统一,所有的页面,不管内容多少,都可以scroll和bounce,符合iOS的交互习惯。可以参看iOS自带的App的交互。
- 6.UITableView隐藏着很多高级特性(继承自UIScrollView),比如automaticallyAdjustsScrollViewInsets让内容可以滚动到导航栏和tabbar的后面,配合磨砂效果,视觉效果非常赞;比如点击状态栏会滚动到顶部;这些特性不需要开发就与生俱来。
- 7.iOS 8新增Self Sizing Cell,只需要设置rowHeight为UITableViewAutomaticDimension就可以实现cell高度根据约束自动调整(务必设置top和bottom与contentView的约束),cell高度计算和缓存已经成为历史往事。
- 8.滚动到页面指定的位置非常方便。并且通过保存UITableview的contentOffset,可以记住页面滚动的位置,当用户进入相同的页面,滚动到之前的位置。
- 9.对于拥有许多输入项的页面,将UITableView的keyboardDismissMode设置成interactive,轻松拥有MessageApp那样灵动的交互。
多用block替代delegate,比如UITableViewCell的点击操作,block比delegate代码更为简洁直观。
- 除了CRUD里面的CUD、下拉刷新和上拉翻页等,绝大部分接口都应该使用缓存,提高用户体验。
- 尽量使用枚举类型要标识状态,提高代码可读性和可维护性。Swift支持字符串作为枚举类型的值,使用根据方便。
- 使用registerClass:forCellReuseIdentifier:+dequeueReusableCellWithIdentifier:forIndexPath:
接口做cell复用。cell可以很多时,一定要坚持做cell复用,提高效率。 - 版本系统API时,通用功能不要引入高版本API。
。
总结
编码规范,没有固定的版本,也没有明确的对错。但是对于完善的开发团队,明确的规范,可以为后期的代码维护,新人学习带来很多好处。还有就是规范,可以将开发维护过程中,前人犯过的错误,进行总结,写入内部编码规范内,起到避免重复犯错的作用。